diff options
Diffstat (limited to 'app/[lng]/evcp')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/bid/[id]/layout.tsx | 2 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/bid/[id]/pre-quote/page.tsx | 11 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/rfq-last/[id]/layout.tsx | 112 | ||||
| -rw-r--r-- | app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx | 67 |
4 files changed, 161 insertions, 31 deletions
diff --git a/app/[lng]/evcp/(evcp)/bid/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/bid/[id]/layout.tsx index b675aed1..eb5e62d0 100644 --- a/app/[lng]/evcp/(evcp)/bid/[id]/layout.tsx +++ b/app/[lng]/evcp/(evcp)/bid/[id]/layout.tsx @@ -61,7 +61,7 @@ export default async function SettingsLayout({ {/* 4) 입찰 정보가 있으면 번호 + 제목 + "상세 정보" 표기 */} <h2 className="text-2xl font-bold tracking-tight"> {bidding - ? `${bidding.biddingNumber ?? ""} - ${bidding.title}` + ? `입찰 No. ${bidding.biddingNumber ?? ""} - ${bidding.title}` : "Loading Bidding..."} </h2> </div> diff --git a/app/[lng]/evcp/(evcp)/bid/[id]/pre-quote/page.tsx b/app/[lng]/evcp/(evcp)/bid/[id]/pre-quote/page.tsx index e2c22b22..64d6d740 100644 --- a/app/[lng]/evcp/(evcp)/bid/[id]/pre-quote/page.tsx +++ b/app/[lng]/evcp/(evcp)/bid/[id]/pre-quote/page.tsx @@ -1,7 +1,8 @@ import { Suspense } from 'react' import { notFound } from 'next/navigation' import { getBiddingDetailData } from '@/lib/bidding/detail/service' -import { BiddingDetailContent } from '@/lib/bidding/detail/table/bidding-detail-content' +import { getBiddingCompanies } from '@/lib/bidding/pre-quote/service' +import { BiddingPreQuoteContent } from '@/lib/bidding/pre-quote/table/bidding-pre-quote-content' // 메타데이터 생성 export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) { @@ -38,13 +39,17 @@ export default async function Page({ params }: PageProps) { notFound() } + // 사전견적용 입찰 업체들 조회 + const biddingCompaniesResult = await getBiddingCompanies(parsedId) + const biddingCompanies = biddingCompaniesResult.success ? biddingCompaniesResult.data : [] + return ( <Suspense fallback={<div className="p-8">로딩 중...</div>}> - <BiddingDetailContent + <BiddingPreQuoteContent bidding={detailData.bidding} quotationDetails={detailData.quotationDetails} quotationVendors={detailData.quotationVendors} - biddingCompanies={detailData.biddingCompanies} + biddingCompanies={biddingCompanies} prItems={detailData.prItems} /> </Suspense> diff --git a/app/[lng]/evcp/(evcp)/rfq-last/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/rfq-last/[id]/layout.tsx index 1b058801..999bfe8b 100644 --- a/app/[lng]/evcp/(evcp)/rfq-last/[id]/layout.tsx +++ b/app/[lng]/evcp/(evcp)/rfq-last/[id]/layout.tsx @@ -4,9 +4,12 @@ import { Separator } from "@/components/ui/separator" import { SidebarNav } from "@/components/layout/sidebar-nav" import { formatDate } from "@/lib/utils" import { Button } from "@/components/ui/button" -import { ArrowLeft } from "lucide-react" +import { Badge } from "@/components/ui/badge" +import { ArrowLeft, Clock, AlertTriangle, CheckCircle, XCircle, AlertCircle } from "lucide-react" import { RfqsLastView } from "@/db/schema" import { findRfqLastById } from "@/lib/rfq-last/service" +import { differenceInDays } from "date-fns" +import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert" export const metadata: Metadata = { title: "견적 목록 상세", @@ -23,30 +26,92 @@ export default async function RfqLayout({ // 1) URL 파라미터에서 id 추출, Number로 변환 const resolvedParams = await params const lng = resolvedParams.lng - const id = resolvedParams.id + const rfqId = parseInt(resolvedParams.id, 10); + + if (!rfqId || isNaN(rfqId) || rfqId <= 0) { + return ( + <div className="p-4"> + <Alert variant="destructive"> + <AlertCircle className="h-4 w-4" /> + <AlertTitle>오류</AlertTitle> + <AlertDescription> + 유효하지 않은 RFQ입니다. + </AlertDescription> + </Alert> + </div> + ); + } + - const idAsNumber = Number(id) // 2) DB에서 해당 협력업체 정보 조회 - const rfq: RfqsLastView | null = await findRfqLastById(idAsNumber) + const rfq: RfqsLastView | null = await findRfqLastById(rfqId) // 3) 사이드바 메뉴 const sidebarNavItems = [ { title: "견적 문서관리", - href: `/${lng}/evcp/rfq-last/${id}`, + href: `/${lng}/evcp/rfq-last/${rfqId}`, }, { title: "RFQ 발송", - href: `/${lng}/evcp/rfq-last/${id}/vendor`, + href: `/${lng}/evcp/rfq-last/${rfqId}/vendor`, }, ] + // Due Date 상태 계산 함수 + const getDueDateStatus = (dueDate: Date | string | null) => { + if (!dueDate) return null; + + const now = new Date(); + const due = new Date(dueDate); + const daysLeft = differenceInDays(due, now); + + if (daysLeft < 0) { + return { + icon: <XCircle className="h-4 w-4" />, + text: `${Math.abs(daysLeft)}일 지남`, + className: "text-red-600", + bgClassName: "bg-red-50" + }; + } else if (daysLeft === 0) { + return { + icon: <AlertTriangle className="h-4 w-4" />, + text: "오늘 마감", + className: "text-orange-600", + bgClassName: "bg-orange-50" + }; + } else if (daysLeft <= 3) { + return { + icon: <AlertCircle className="h-4 w-4" />, + text: `${daysLeft}일 남음`, + className: "text-amber-600", + bgClassName: "bg-amber-50" + }; + } else if (daysLeft <= 7) { + return { + icon: <Clock className="h-4 w-4" />, + text: `${daysLeft}일 남음`, + className: "text-blue-600", + bgClassName: "bg-blue-50" + }; + } else { + return { + icon: <CheckCircle className="h-4 w-4" />, + text: `${daysLeft}일 남음`, + className: "text-green-600", + bgClassName: "bg-green-50" + }; + } + }; + + const dueDateStatus = rfq?.dueDate ? getDueDateStatus(rfq.dueDate) : null; + return ( <> <div className="container py-6"> <section className="overflow-hidden rounded-[0.5rem] border bg-background shadow"> <div className="hidden space-y-6 p-10 pb-16 md:block"> - <div className="flex items-center justify-end mb-4"> + <div className="flex items-center justify-end mb-4"> <Link href={`/${lng}/evcp/rfq-last`} passHref> <Button variant="ghost" className="flex items-center text-primary hover:text-primary/80 transition-colors p-0 h-auto"> <ArrowLeft className="mr-1 h-4 w-4" /> @@ -55,25 +120,38 @@ export default async function RfqLayout({ </Link> </div> <div className="space-y-0.5"> - {/* 4) 협력업체 정보가 있으면 코드 + 이름 + "상세 정보" 표기 */} + {/* 제목 로직 수정: rfqTitle 있으면 사용, 없으면 rfqCode만 표시 */} <h2 className="text-2xl font-bold tracking-tight"> {rfq - ? `${rfq.rfqCode ?? ""} | ${rfq.packageNo ?? ""} | ${rfq.packageName ?? ""}` + ? rfq.rfqTitle + ? `견적 상세 관리 ${rfq.rfqCode ?? ""} | ${rfq.rfqTitle}` + : `견적 상세 관리 ${rfq.rfqCode ?? ""}` : "Loading RFQ..."} </h2> - - <p className="text-muted-foreground"> - RFQ 관리하는 화면입니다. - </p> - <h3>Due Date:{rfq && rfq?.dueDate && <strong>{formatDate(rfq?.dueDate, "KR")}</strong>}</h3> + + {/* <p className="text-muted-foreground"> + RFQ 관리하는 화면입니다. + </p> */} + + {/* Due Date 표시 개선 */} + {rfq?.dueDate && dueDateStatus && ( + <div className="flex items-center gap-3 pt-2"> + <span className="text-sm font-medium text-muted-foreground">Due Date:</span> + <strong className="text-sm">{formatDate(rfq.dueDate, "KR")}</strong> + <div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full ${dueDateStatus.bgClassName} ${dueDateStatus.className}`}> + {dueDateStatus.icon} + <span className="text-xs font-medium">{dueDateStatus.text}</span> + </div> + </div> + )} </div> <Separator className="my-6" /> <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0"> - <aside className="lg:w-64 flex-shrink-0"> - <SidebarNav items={sidebarNavItems} /> + <aside className="lg:w-64 flex-shrink-0"> + <SidebarNav items={sidebarNavItems} /> </aside> <div className="lg:w-[calc(100%-16rem)] overflow-auto">{children}</div> - </div> + </div> </div> </section> </div> diff --git a/app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx b/app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx index 6819e122..1ccb7559 100644 --- a/app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx +++ b/app/[lng]/evcp/(evcp)/rfq-last/[id]/page.tsx @@ -3,6 +3,9 @@ import { type SearchParams } from "@/types/table" import { getValidFilters } from "@/lib/data-table" import { searchParamsRfqAttachmentsCache } from "@/lib/b-rfq/validations" import { getRfqLastAttachments } from "@/lib/rfq-last/service" +import { RfqAttachmentsTable } from "@/lib/rfq-last/attachment/rfq-attachments-table" +import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert" +import { AlertCircle } from "lucide-react" interface IndexPageProps { // Next.js 13 App Router에서 기본으로 주어지는 객체들 @@ -16,21 +19,61 @@ interface IndexPageProps { export default async function RfqPage(props: IndexPageProps) { const resolvedParams = await props.params const lng = resolvedParams.lng - const id = resolvedParams.id + const rfqId = parseInt(resolvedParams.id, 10); - const idAsNumber = Number(id) + if (!rfqId || isNaN(rfqId) || rfqId <= 0) { + return ( + <div className="p-4"> + <Alert variant="destructive"> + <AlertCircle className="h-4 w-4" /> + <AlertTitle>오류</AlertTitle> + <AlertDescription> + 유효하지 않은 RFQ입니다. + </AlertDescription> + </Alert> + </div> + ); + } // 2) SearchParams 파싱 (Zod) // - "filters", "page", "perPage", "sort" 등 contact 전용 컬럼 - const searchParams = await props.searchParams - const search = searchParamsRfqAttachmentsCache.parse(searchParams) - const validFilters = getValidFilters(search.filters) + const searchParams = await props.searchParams; + const activeTab = searchParams.tab || '설계'; + + // 활성 탭에 따라 다른 파라미터 파싱 + const designSearch = activeTab === '설계' + ? searchParamsRfqAttachmentsCache.parse({ + ...searchParams, + // design_ prefix가 붙은 파라미터들 추출 + page: searchParams.design_page, + perPage: searchParams.design_perPage, + sort: searchParams.design_sort, + filters: searchParams.design_filters, + }) + : { page: 1, perPage: 10, sort: [], filters: [] }; + + const purchaseSearch = activeTab === '구매' + ? searchParamsRfqAttachmentsCache.parse({ + ...searchParams, + // purchase_ prefix가 붙은 파라미터들 추출 + page: searchParams.purchase_page, + perPage: searchParams.purchase_perPage, + sort: searchParams.purchase_sort, + filters: searchParams.purchase_filters, + }) + : { page: 1, perPage: 10, sort: [], filters: [] }; + + // 활성 탭의 데이터만 실제로 가져오기 + const [designData, purchaseData] = await Promise.all([ + activeTab === '설계' + ? getRfqLastAttachments({ ...designSearch }, rfqId, "설계") + : { data: [], pageCount: 0 }, + activeTab === '구매' + ? getRfqLastAttachments({ ...purchaseSearch }, rfqId, "구매") + : { data: [], pageCount: 0 } + ]); - const promises = getRfqLastAttachments({ - ...search, - filters: validFilters, - }, idAsNumber) // 4) 렌더링 return ( @@ -45,7 +88,11 @@ export default async function RfqPage(props: IndexPageProps) { </div> <Separator /> <div> - {/* <RfqAttachmentsTable promises={promises} rfqId={idAsNumber} /> */} + <RfqAttachmentsTable + rfqId={rfqId} + initialDesignData={designData} + initialPurchaseData={purchaseData} + /> </div> </div> ) |
